package com.robot.console.client;

import com.go.core.Message;
import com.go.logging.Logs;
import com.go.threading.ContextThreadFactory;
import com.robot.console.client.actions.*;
import com.robot.console.client.common.ISingleClientAction;
import com.robot.console.client.common.SingleClientActionsContainer;
import com.robot.console.client.common.SingleClientProcessorsContainer;
import com.robot.console.client.processors.*;
import com.robot.console.config.CommendParams;
import com.robot.console.config.CsConfig;
import com.robot.console.core.ClientCommonParam;
import com.robot.console.core.ClientInfo;
import com.robot.console.core.ClientMessageHandler;
import com.robot.console.core.ICallback;
import org.jboss.netty.bootstrap.ClientBootstrap;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelUpstreamHandler;
import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory;
import org.jboss.netty.handler.execution.MemoryAwareThreadPoolExecutor;
import org.jboss.netty.handler.execution.OrderedMemoryAwareThreadPoolExecutor;
import org.jboss.netty.handler.timeout.IdleState;

import java.net.InetSocketAddress;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import static com.google.common.base.Preconditions.checkNotNull;

public class Client
{
	private class HeartBeat extends Thread
	{
		private static final long ONE_MINUTE = 60000L;

		public HeartBeat()
		{
			this.setName("Heart beat thread");
		}

		@Override
		public void run()
		{
			while (!Thread.currentThread().isInterrupted())
			{
				try
				{
					Thread.sleep(ONE_MINUTE);
					Client.this.heartBeat();
				}
				catch (Exception ex)
				{
					if (ex instanceof InterruptedException)
					{
						Thread.currentThread().interrupt();
					}
				}
			}
		}
	}

	private final SingleClientActionsContainer actionsContainer = new SingleClientActionsContainer();
	private ClientBootstrap bootstrap;
	private Channel channel;
	private ICallback<String> channelStateCallBack;
	private final ClientInfo clientInfo;
	private final int eventThreads;
	private String host;
	private ChannelUpstreamHandler ioHandler;
	private final int ioThreads;

	public static boolean zip;

	private boolean neededReconnect;
	private int port;
	private final SingleClientProcessorsContainer processorsContainer = new SingleClientProcessorsContainer();

	public Client(CsConfig config, ClientInfo info)
	{
		checkNotNull(config);
		checkNotNull(info);

		this.setHost(config.getCsServerHost());
		this.setPort(config.getCsServerPort());
		this.ioThreads = config.getIoThreads();
		this.eventThreads = 1; // config.getEventThreads();
		this.clientInfo = info;
		zip = config.isNeedZip();

	}

	public void channelExceptionCaught(Throwable cause)
	{
		ClientCommonParam.printLogBack(cause.getMessage());
	}

	public void channelIdleState(IdleState state)
	{
		ClientCommonParam.printLogBack("Client channel is on idle state");
	}

	private boolean channelIsConnected()
	{
		return (channel != null && channel.isConnected());
	}

	private boolean channelNotConnected()
	{
		return (channel == null || !channel.isConnected());
	}

	public void channelStateChanged(String channelConnect, Channel channel)
	{
		InetSocketAddress address = (InetSocketAddress) channel.getRemoteAddress();
		if (channelConnect.equalsIgnoreCase(ClientMessageHandler.CHANNEL_CONNECT))
		{
			this.channel = channel;

			ClientCommonParam.printLogBack(
					"Client connect to server : " + address.getHostString() + ":" + address.getPort(),
					ClientCommonParam._FILE_(), String.valueOf(ClientCommonParam._LINE_()));
		}
		else if (channelConnect.equalsIgnoreCase(ClientMessageHandler.CHANNEL_DISCONNECT))
		{
			this.channel = null;
			clientInfo.setAuthed(false);
			ClientCommonParam.printLogBack(
					"Client disconnect from server : " + address.getHostString() + ":" + address.getPort(),
					ClientCommonParam._FILE_(), String.valueOf(ClientCommonParam._LINE_()));
		}

		if (this.getChannelStateCallBack() != null && this.neededReconnect)
		{
			getChannelStateCallBack().handleResult(channelConnect);
		}
	}

	public void connect()
	{
		if (bootstrap == null)
		{
			ClientCommonParam.printLogBack("Client is still no inited");
			return;
		}

		if (channelIsConnected())
		{
			ClientCommonParam.printLogBack("Client already connect to server");
			return;
		}

		this.bootstrap.connect(new InetSocketAddress(getHost(), getPort()));
	}

	public void connect(String address)
	{
		if (bootstrap == null)
		{
			ClientCommonParam.printLogBack("Client is still not inited");
			return;
		}

		String[] addressArray = address.split(":");
		if (addressArray.length != 2)
		{
			ClientCommonParam.printLogBack("Error information with : " + address);
			return;
		}

		this.bootstrap.connect(new InetSocketAddress(addressArray[0], Integer.parseInt(addressArray[1])));
	}

	public void disconnect()
	{
		if (channelNotConnected())
		{
			ClientCommonParam.printLogBack("Client is not connect to server");
			return;
		}
		this.channel.disconnect();
	}

	public SingleClientActionsContainer getActionsContainer()
	{
		return actionsContainer;
	}

	public Channel getChannel()
	{
		return channel;
	}

	public ICallback<String> getChannelStateCallBack()
	{
		return channelStateCallBack;
	}

	public ClientInfo getClientInfo()
	{
		return clientInfo;
	}

	public String getHost()
	{
		return host;
	}

	public int getPort()
	{
		return port;
	}

	private SingleClientProcessorsContainer getProcessorsContainer()
	{
		return this.processorsContainer;
	}

	private void heartBeat()
	{
		if (clientInfo.isAuthed())
		{
			takeAction(PingAction.KEY);
		}
	}

	public void init()
	{
		initContainers();

		this.ioHandler = new ClientMessageHandler(this);

		ExecutorService bossExecutor = Executors.newCachedThreadPool();
		ExecutorService workerExecutor = new MemoryAwareThreadPoolExecutor(ioThreads, 0, 0, 300L, TimeUnit.SECONDS,
				new ContextThreadFactory("netty connector worker thread "));
		ExecutorService eventExecutor = new OrderedMemoryAwareThreadPoolExecutor(eventThreads, 0, 0, 300L,
				TimeUnit.SECONDS, new ContextThreadFactory("netty connector event thread "));

		this.bootstrap = new ClientBootstrap(new NioClientSocketChannelFactory(bossExecutor, workerExecutor, ioThreads));
		this.bootstrap.setPipelineFactory(new ClientPipelineFactory(eventExecutor, ioHandler));
		this.bootstrap.setOption("reuseAddress", true);
		this.bootstrap.setOption("child.reuseAddress", true);
		this.bootstrap.setOption("writeBufferHighWaterMark", 64 * 1024);
		this.bootstrap.setOption("writeBufferLowWaterMark", 32 * 1024);
		this.bootstrap.setOption("child.tcpNoDelay", true);
		this.bootstrap.setOption("child.sendBufferSize", 65536);
		this.bootstrap.setOption("child.receiveBufferSize", 65536);

		HeartBeat heartBeat = new HeartBeat();
		heartBeat.start();
	}

	private void initContainers()
	{
		this.actionsContainer.addAction(new AuthAction());

		this.actionsContainer.addAction(new TalkMsgAction());
		this.actionsContainer.addAction(new PingAction());
		this.actionsContainer.addAction(new EchoAction());
		this.actionsContainer.addAction(new InitRoomAction());
		this.actionsContainer.addAction(new GetLWExercisesAction());
		this.actionsContainer.addAction(new EnterRoomAction());
		this.actionsContainer.addAction(new QuitAction());
		this.actionsContainer.addAction(new SignInAction());
		this.actionsContainer.addAction(new RaiseHandAction());
		this.actionsContainer.addAction(new MakeFriendAction());
		this.actionsContainer.addAction(new SubmitProblemAction());
		this.actionsContainer.addAction(new FavorProblemAction());
		this.actionsContainer.addAction(new CreateRoomAction());
		this.actionsContainer.addAction(new DestroyRoomAction());

		this.actionsContainer.addAction(new TeacherEnterRoomAction());

		this.processorsContainer.addMessageProcessor(AuthProcessor.KEY, new AuthProcessor());
		this.processorsContainer.addMessageProcessor(ReconnectProcessor.KEY, new ReconnectProcessor());
		this.processorsContainer.addMessageProcessor(TalkMsgProcessor.KEY, new TalkMsgProcessor());
		this.processorsContainer.addMessageProcessor(DMode.KEY, new DMode());
		this.processorsContainer.addMessageProcessor(CommendParams.ECHO, new EchoProcessor());
		this.processorsContainer.addMessageProcessor(InitRoomProcessor.KEY, new InitRoomProcessor());
		this.processorsContainer.addMessageProcessor(EnterRoomProcessor.KEY, new EnterRoomProcessor());
		this.processorsContainer.addMessageProcessor(QuitRoomProcessor.KEY, new QuitRoomProcessor());
	}

	public void messageReceived(Message message)
	{
		ClientCommonParam.printLogBack("client receive : " + message.toString());
	}

	public void Reconnect()
	{
		this.neededReconnect = true;

		this.setChannelStateCallBack(new ICallback<String>()
		{
			@Override
			public void handleResult(String channelConnect)
			{
				if (channelConnect.equalsIgnoreCase(ClientMessageHandler.CHANNEL_CONNECT))
				{
					Client.this.neededReconnect = false;
					Client.this.takeAction(CommendParams.AUTH);
				}
				else if (channelConnect.equalsIgnoreCase(ClientMessageHandler.CHANNEL_DISCONNECT))
				{
					Client.this.connect();
				}

			}
		});

		this.disconnect();
	}

	public void send(Message content)
	{
		ClientCommonParam.printLogBack("client send :" + content.toString());
		try
		{
			ChannelBuffer duplicateBuffer = null;
			if (zip)
			{
				duplicateBuffer = content.duplicateSourceBuffer();
			}
			else
			{
				duplicateBuffer = content.duplicateUnzipChannelBuffer();
			}
			this.channel.write(duplicateBuffer);
		}
		catch (Exception e)
		{
			Logs.warn("Catch an exception " + e);
		}
	}

	public void setChannelStateCallBack(ICallback<String> channelStateCallBack)
	{
		this.channelStateCallBack = channelStateCallBack;
	}

	public void setHost(String host)
	{
		this.host = host;
	}

	public void setPort(int port)
	{
		this.port = port;
	}

	public void shutdown()
	{
		if (channelNotConnected())
		{
			return;
		}
		channel.close();

		if (bootstrap != null)
		{
			bootstrap.shutdown();
		}
	}

	public void takeAction(String param)
	{
		checkNotNull(param);

		if (channelNotConnected())
		{
			ClientCommonParam.printLogBack("Client is not connected, can not take an action");
			return;
		}

		if ("".equals(param))
		{
			ClientCommonParam.printLogBack("Action's params can not be empty");
			return;
		}

		String[] params = param.split(" ");
		String actionName = params[0];
		String actionArgs = "";
		if (params.length > 1)
		{
			actionArgs = param.substring(actionName.length() + 1);
		}

		ISingleClientAction action = this.getActionsContainer().getAction(actionName.toLowerCase());

		if (action == null)
		{
			action = this.getActionsContainer().getAction(CommendParams.TALK_MSG.toLowerCase());
		}

		Message message = action.prepareMessage(actionArgs);

		addClientInfo(message);

		send(message);

	}

	private void addClientInfo(Message message)
	{
		message.setAttribute(CommendParams.ATTR_MSUID, clientInfo.getUserId());// user_id
		message.setAttribute(CommendParams.TOKEN, clientInfo.getToken());// token
		// message.setAttribute(CommendParams.VERSION,
		// CommendParams.VERSION_NUMBER);// token
	}

}
